Introducción

Para esta segunda práctica nos encontrábamos frente a un problema de riesgo de crédito, el cual permite predecir mediante probabilidad la posibilidad de incurrir en una pérdida debido a un incumplimiento de un futuro crédito que se desee brindar. Para este ejercicio, se contó con la base de datos loan_data_2007_2014.csv obtenida a través de kaggle. Esta contiene información perteneciente a usuarios entre los años 2007 y 2014 de lendingclub, una empresa que realiza préstamos digitales en Estados Unidos ( lendingclub ).

Objetivo

El objetivo principal de la práctica fue el de realizar un modelo de probabilidad el cual permitiese predecir la probabilidad de que un individuo incumpla sus obligaciones financieras en los siguientes 12 meses desde que se genere el crédito.

También se debía representar este mismo modelo con un Scorecard, el cual es un valor numérico que sirve para medir la solvencia de un individuo. De igual forma se debía analizar qué variables hacen más riesgosa a una persona. Y finalmente, se debía desarrollar una aplicación web que le permitiera a los usuario ver su calificación de scorecard, de acuerdo a sus características, y cómo se encuentra respecto al resto de la población.

import pandas as pd
import numpy as np
import seaborn as sns
from scipy.stats import chi2_contingency
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import plotly.express as px
from sklearn.naive_bayes import GaussianNB
from tqdm import tqdm
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
mes_name=['Dec', 'Nov', 'Oct', 'Sep', 'Aug', 'Jul', 'Jun', 'May', 'Apr', 'Mar', 'Feb', 'Jan']
mes_num=list(pd.Series(list((13-np.arange(1,13)))).astype("str"))
def format_replace(string_): # ajust formato de fecha
    for i in range(0,12):
        string_=str(string_).replace(mes_name[i],mes_num[i] )
    return string_
def dummy_creation(df_, columns_list): # creacion de variables dummy
    df_dummies = []
    for col in columns_list:
        df_dummies.append(pd.get_dummies(df_[col], prefix = col, prefix_sep = ':',drop_first=True))
    df_dummies = pd.concat(df_dummies, axis = 1)
    df_ = pd.concat([df_, df_dummies], axis = 1)
    df_=df_.drop(labels=columns_list,axis=1)
    return df_
def plot_barplot(df_temp, x, y,nrow,ncol,fig_temp,show_leg):
    df1 = df_temp.groupby(x)[y].value_counts(normalize=True)
    df1 = df1.mul(100)
    df1 = df1.rename('percent').reset_index()
    df_temp=df1[df1[y]==1].copy()
    fig_temp.add_trace(go.Bar(x=df_temp[x],
                y=df_temp["percent"],
                name="1",
                marker_color='rgb(55, 83, 109)',
                              hovertext =(x,"percent" ),
                              showlegend=show_leg
                ), row=nrow,col=ncol)
    df_temp=df1[df1[y]==0].copy()
    fig_temp.add_trace(go.Bar(x=df_temp[x],
                y=df_temp["percent"],
                name="0",
                marker_color='rgb(26, 118, 255)',
                              hovertext =(x,"percent" ),
                              showlegend=show_leg
                ),row=nrow,col=ncol)
    fig_temp.update_yaxes(range = [0,100])
    return fig_temp

Depuración

df=pd.read_csv("loan_data_2007_2014.csv")
## <string>:1: DtypeWarning:
## 
## Columns (19) have mixed types. Specify dtype option on import or set low_memory=False.

La base de datos loan_data_2007_2014.csv cuenta con 74 columnas y 466285 registros.

  • Las columnas id, member_id, url, title hacen referencia a información de identificación del usuario, que no es de interés para el estudio.

Variables primordiales

Para la creación del modelo se tienen:

  • issue_d: El mes en que se financió el préstamo (mes-año).

  • last_pymnt_d: El último mes de pago fue recibido.

  • loan_satus: Esta sera la variable objetivo, cuenta con 9 categorías que clasifican el ultimo estado registrado.

Creación de variables

Como el objetivo es crear un modelo para predecir si al cabo de 12 meses que se origina el credíto (issue_d) el usuario incumple sus obligaciones financieras, luego de analizar las variables fecha registradas se crea month_last: meses que han pasado desde el ultimo pago, que es la diferencia (last_pymnt_d-issue_d ) esto nos dará informción del tiempo que pago el usuario y con la variable loan_status se podrá saber si el usuario incumple entre el tiempo de interés (antes de 12 meses).

Se crea las variables.

  • good_status: Que tomara valores 0 (con morosidad) y 1 (Sin morosidad)
#results="asis"
table_frec=pd.DataFrame(df["loan_status"].value_counts())
status_mora=['Charged Off', 'Default', 'Late (31-120 days)','Does not meet the credit policy. Status:Charged Off']
table_frec["good_status"]=1
filtro=pd.Series(table_frec.index).isin( status_mora)
table_frec.loc[list(filtro), "good_status"]=0
table_frec=table_frec.reset_index()
table_frec.columns=["loan_status", "Frec", "good_status" ]
df_temp=table_frec[["loan_status", "good_status","Frec" ]]
df["good_status"]=1
df.loc[df["loan_status"].isin(status_mora),"good_status"]=0
Categorías good_status
loan_status good_status Frec
Current 1 224226
Fully Paid 1 184739
Charged Off 0 42475
Late (31-120 days) 0 6900
In Grace Period 1 3146
Does not meet the credit policy. Status:Fully Paid 1 1988
Late (16-30 days) 1 1218
Default 0 832
Does not meet the credit policy. Status:Charged Off 0 761
  • month_last: last_pymnt_d-issue_d en meses.
df[ 'issue_d']=pd.to_datetime(df['issue_d'], format = "%m-%y")
df['last_pymnt_d']=pd.to_datetime(df['last_pymnt_d'], format = "%m-%y")
df["month_last"]= ((df.last_pymnt_d - df.issue_d)/np.timedelta64(1, 'M'))
  • target_time: 1 si month_last \(\leq\) 12, 0 si >12.
df['target_time']=0
df.loc[df["month_last"]<=12,'target_time']=1

Estructura del modelo

El modelo general tendra la estructura:

\[ P(\text{good_status=1} )= f(\text{month_last}, {X },\theta ) \] Donde month_last define el tiempo en que queremos predecir, \(X\) es un vector de variables que puedan afectar la probabilidad y \(\theta\) son los parámetros que puede contener el modelo.

Valores NA

Asumiendo que se tolera al menos un 20 % de valores NA en los datos de las y omitiendo las columnas de identificación se cuenta con:

drop_columns=["id", "member_id", "url", "title"]
df=df.drop(labels=drop_columns,axis=1 )
total_na=df.isna().sum()
filtro=total_na< df.shape[0]*0.2
total_na=total_na[filtro]
result=pd.DataFrame({"Variables":["Menos del 20% NA"," Mas del 20% NA"],
"Total variables":[total_na.shape[0], 70-total_na.shape[0] ]} )
Resumen de NA
Variables Total variables
Menos del 20% NA 51
Mas del 20% NA 19

Se omiten 22 variables por su alto porcentaje de valores faltantes, aunque 20% de valores faltantes es una cantidad alta, existen variables importantes que contienen alta cantidad de valores faltantes que se muestran a continuación.

Variables modelo

vars_=total_na.sort_values(ascending=False).head()
result=pd.DataFrame({"Variables":vars_.index,"Descripción":[" Total crédito rotativo alto entre límite de crédito. ","Saldo corriente en todas las cuentas ", " Montos totales de cobro adeudados. ", "Tipo de trabajo.","Años en el trabajo "   ], "Total NA":vars_ })
result=result.reset_index()
Variables candidatas
index Variables Descripción Total NA
total_rev_hi_lim total_rev_hi_lim Total crédito rotativo alto entre límite de crédito. 70276
tot_cur_bal tot_cur_bal Saldo corriente en todas las cuentas 70276
tot_coll_amt tot_coll_amt Montos totales de cobro adeudados. 70276
emp_title emp_title Tipo de trabajo. 27588
emp_length emp_length Años en el trabajo 21008

De estas variables puede ser dificil que el usuario obtenga total_rev_hi_lim, emp_title tiene muchas categorías.

Es importante identificar que variables puede dar un usuario al momento del registro, pues existen variables donde se obtienen la información al pasar el tiempo o un usuario no puede identificar.

Variables que no se incluyen
funded_amnt_inv grade sub_grade emp_title
verification_status zip_code addr_state dti
delinq_2yrs inq_last_6mths revol_bal revol_util
total_acc out_prncp out_prncp_inv out_prncp_inv
total_pymnt total_rec_prncp total_rec_int total_rec_late_fee
recoveries collection_recovery_fee last_pymnt_amnt last_credit_pull_d
collections_12_mths_ex_med policy_code tot_coll_amt total_rev_hi_lim

Las variables en la tabla no se tienen en cuenta porque son medidas que son proporcionadas por LC, información al pasar el tiempo después del prestamo o son extraidas de un externo, por ende, las variables a considerar como influyentes en el incumplimiento de las finanzas son:

Covariables candidatas
Variables Descripción
loan_amnt El monto indicado del préstamo solicitado por el prestatario. Si en algún momento, el departamento de crédito reduce el monto del préstamo, entonces se reflejará en este valor.
funded_amnt El monto total comprometido con ese préstamo en ese momento.
term El número de pagos del préstamo. Los valores son en meses y pueden ser 36 o 60.
int_rate tasa de interés del préstamo.
installment cuota El pago mensual adeudado por el prestatario si el préstamo se origina.
home_ownership El estado de propiedad de la vivienda proporcionado por el prestatario durante el registro. Nuestros valores son
annual_inc Los ingresos anuales autoinformados proporcionados por el prestatario durante el registro.
earliest_cr_line El mes en que se abrió la primera línea de crédito reportada del prestatario.
open_acc El número de líneas de crédito abiertas en el archivo de crédito del prestatario.
pub_rec numero de derogatory public records.
acc_now_delinq El número de cuentas en las que el prestatario está ahora en mora.
purpose Razón por la que se hace el prestamo.
tot_cur_bal Saldo corriente total de todas las cuentas
emp_length años trabajo
initial_list_status El estado inicial de listado del préstamo. Los valores posibles son – W, F
pymnt_plan indica si se a establecido un plan de pago.

En esta tabla se tienen las posibles variables para el modelo, con good_status y target_time.

variables_=pd.Series(variables_).apply(lambda x: x.replace("__","" ))
df["month_earliest_cr_line"]=((df.issue_d-df.earliest_cr_line )/np.timedelta64(1, 'M'))
df=df[ [*variables_, "good_status","target_time","month_earliest_cr_line" ]] 
df=df[~df.isna().any(axis=1)]
result=pd.DataFrame({"":["Filas", "Columnas"] ,"Cantidad":df.shape})
Dimensión de datos
Cantidad
Filas 377062
Columnas 19

Como resultado para el modelo se usara un data frame con estas dimesiones.

Analisis Descriptivo

¿Cuál es la distribución de good_estatus?

tabla_1=df.good_status.value_counts(normalize=True)*100
tabla_2=df.good_status.value_counts()
tabla_3=df.target_time.value_counts(normalize=True)*100
tabla_4=df.target_time.value_counts()
table_final=pd.DataFrame({"good status":[1,0],"Frecuencia":tabla_2, 
                          "Frec %":tabla_1, "target time":[0,1],
                         "Frecuencia ":tabla_4, "Frec % ":tabla_3 } )
Distribución global
good status Frecuencia Frec % target time Frecuencia Frec %
1 37709 10 0 305925 81.13
0 339353 90 1 71137 18.87

La variable acc_now_delinq se puede transformar en 1 si tiene al menos una cuenta en mora, 0 sino tiene cunetas en mora. También la variable pub_rec se transforma 1 si tiene al menos un derogatory public records, 0 sino. Se tomo la decisión ya que son variables conteos donde no parece ser necesario. Como esta es la fecha en que se hizo su primer prestamo earliest_cr_line, se debe calcular los meses que han pasado desde que solicito el prestamo month_earliest_cr_line

df=df.drop(labels="earliest_cr_line",axis=1)
df["acc_now_delinq"]=np.where(df["acc_now_delinq"]>0, 1,0)
df["pub_rec"]=np.where(df["pub_rec"]>0,1,0)
df_temp=df.copy()
df_temp["acc_now_delinq"]=df_temp["acc_now_delinq"].astype("str")
df_temp["target_time"]=df_temp["target_time"].astype("str")
df_temp["pub_rec"]=df_temp["pub_rec"].astype("str")
X=df_temp.drop(labels="good_status",axis=1)
Y=df_temp["good_status"]

X_train_cat = X.select_dtypes(include = 'object').copy()
X_train_num = X.select_dtypes(include = 'number').copy()
# define an empty dictionary to store chi-squared test results
chi2_check = {}

# loop over each column in the training set to calculate chi-statistic with the target variable
for column in X_train_cat:
    chi, p, dof, ex = chi2_contingency(pd.crosstab(Y, X_train_cat[column]))
    chi2_check.setdefault('Feature',[]).append(column)
    chi2_check.setdefault('p-value',[]).append(round(p, 10))

# convert the dictionary to a DF
chi2_result = pd.DataFrame(data = chi2_check)
chi2_result.sort_values(by = ['p-value'], ascending = True, ignore_index = True, inplace = True)
Prueba
Feature p-value
term 0.00
home_ownership 0.00
purpose 0.00
emp_length 0.00
initial_list_status 0.00
target_time 0.00
pub_rec 0.00
pymnt_plan 0.14
acc_now_delinq 0.51

Se realiza pruebas \(\chi^2\) para las variables categoricas en contraste con la variable good_status y se observa 8 variables con un p-valor pequeño, esto significa que estas variables pueden influir en el incumplimiento de las finanzas. Se excluye pymnt_plan debido a un p-valor > 0.05.

corrmat = X_train_num.corr()
sns.heatmap(corrmat)
Correlación entre variables númericas.

Correlación entre variables númericas.

En la figura 1 se observa que hay 3 variables con una alta correlación entre si (\(\approx\) 1) que son: loan_amnt, funded_amnt, installment y según la table - 5 se opta por loan_amnt por ser el prestamo definitivo que dio LC.

vars_=['loan_amnt',  'int_rate', 'annual_inc',
       'open_acc', 'tot_cur_bal',"month_earliest_cr_line"]
fig, axs = plt.subplots(ncols=3,nrows=2,figsize=(18, 7))
sns.boxplot(data=df, x="good_status", y=vars_[0], ax=axs[0, 0],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[1], ax=axs[0, 1],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[2], ax=axs[0, 2],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[3], ax=axs[1, 1],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[4], ax=axs[1, 2],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[5], ax=axs[1, 0],showfliers = False)
Box plot comparativos dado good_status

Box plot comparativos dado good_status

En la figura se observa que las variables con una tendencia de influir en good_status son int_rate, annual_inc pues aunque no se observa gran diferencia, parece existir una influencia.

Las variables a usar para crear el modelo son:

variables_final=list(chi2_result["Feature"].iloc[0:8])
variables_final=[*variables_final,*vars_,"good_status"]
result=np.array([*variables_final,""])
result.shape=(4,4)
result=pd.DataFrame(result)
result.columns=[""]*4
Covariables del modelo
term home_ownership purpose emp_length
initial_list_status target_time pub_rec pymnt_plan
loan_amnt int_rate annual_inc open_acc
tot_cur_bal month_earliest_cr_line good_status

Estas son las covariables que se tendrán en el modelo.

df.loc[df["home_ownership"] == "ANY","home_ownership"] = 'NONE'
var_cat=['term','pub_rec','home_ownership','initial_list_status','purpose','emp_length']
fig_ = make_subplots(rows=4, cols=2,subplot_titles =var_cat)
fig_ =plot_barplot(df, var_cat[0],"good_status", 1,1,fig_,True)
fig_ =plot_barplot(df, var_cat[1],"good_status", 1,2,fig_, True)
fig_ =plot_barplot(df, var_cat[2],"good_status", 2,1,fig_, True)
fig_ =plot_barplot(df, var_cat[3],"good_status", 2,2,fig_, True)
fig_ =plot_barplot(df, var_cat[4],"good_status", 3,1,fig_, True)
fig_ =plot_barplot(df, var_cat[5],"good_status", 3,2,fig_, True)
# fig_ =plot_barplot(df, var_cat[6],"good_status", 4,1,fig_, True)
fig_.update_layout(
    title="Good status",
    xaxis_tickfont_size=14,
    yaxis=dict(
        title='Distribution percent ',
        titlefont_size=16,
        tickfont_size=14,
    ),
    legend=dict(
        x=1,
        y=1.0,
        bgcolor='rgba(255, 255, 255, 0)',
        bordercolor='rgba(255, 255, 255, 0)'
    ),
    barmode='group',
    bargap=0.15, # gap between bars of adjacent location coordinates.
    bargroupgap=0.1, # gap between bars of the same location coordinate.
    height=1000, width=900
    )

¿Cómo afectan las variables?

  • Cuando el número de pagos es \(>\) 36 meses la probabilidad de incumplimiento es mayor, es decir, un usuario puede inclumir si tiene mas número de pagos al inicio del prestamo.

  • Si el usuario marca NONE o OTHER o RENT influye negativamente, es decir, aumenta la probabilidad de que incumpla sus obligaciones financieras.

  • Si el proposito del prestamo es small_business, house, weddlng aumenta la probabilidad de que incumpla sus obligaciones financieras.

  • A medida que el usuario tiene menor tiempo de trabajo, aumenta la probabilidad de que incumpla sus obligaciones financieras.

Modelos

Se plantean diferentes modelos aplicando validación cruzada 80% prueba 20% test.

Luego de ensayar modelos se encontro que el valor de probabilidad de cambio mas adecuado es 0.8, es decir, si la probabilidad >=0.8 good_status 1, por el contrario 0.

Partición de datos

df_modelo=df[variables_final].copy()
df_modelo.loc[df_modelo["home_ownership"] == "ANY","home_ownership"] = 'NONE'
X = df_modelo.drop('good_status', axis = 1).copy()
X=dummy_creation(X, ["term","home_ownership", "purpose",'initial_list_status','emp_length' ,"pymnt_plan"])
y = df_modelo['good_status'].copy()
# y= np.where(y==1,0,1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 421)#, stratify = y)
X_train, X_test = X_train.copy(), X_test.copy()
result=pd.DataFrame({"":["Filas", "Columnas"] ,"Trian":X_train.shape,"Test":X_test.shape })
Dimensiones partición
Trian Test
Filas 301649 75413
Columnas 37 37

Modelo Naive Bayes

De las variables que se escogieron se probo eliminando variables y para el modelo las variables selecionadas son: target_time, pub_rec, loan_amnt, int_rate, open_acc, home_ownership.

var_=[
    'target_time', 
      'pub_rec', 
      'loan_amnt', 
      'int_rate', 
       'open_acc', 
       'home_ownership:NONE', 'home_ownership:OTHER', 'home_ownership:OWN',
       'home_ownership:RENT', 
     ]
X_train_pca=X_train[var_]
# X_train_pca=PCA_5.transform(X_train)
X_test_pca=X_test[var_]
# X_test_pca=PCA_5.transform(X_test)
clf = GaussianNB()
clf.fit(X_train_pca, y_train)
## GaussianNB()
prop_=0.8
predict_train_nb= clf.predict_proba(X_train_pca)[:,1]
predict_test_nb= clf.predict_proba(X_test_pca)[:,1]
predict_train_nb=np.where(predict_train_nb>=prop_,1,0  )
predict_test_nb=np.where(predict_test_nb>=prop_,1,0  )
tasa_acierto_1=accuracy_score(y_train, predict_train_nb)*100
tasa_acierto_2=accuracy_score(y_test,predict_test_nb )*100
result=pd.DataFrame({"Población":["Train", "Test"], "Aciertos": [tasa_acierto_1,tasa_acierto_2 ] })
errores_bayes=pd.concat([pd.crosstab(y_train,predict_train_nb ,margins=False, normalize=True, colnames=["predicción"],  rownames=['Real']),
           pd.crosstab(y_test,predict_test_nb , margins=False, normalize=True, colnames=["predicción"], rownames=['Real'])
    ],axis=1,keys=["Train", "Test"])
Validación modelo Naive Bayes
Población Aciertos
Train 85.11
Test 85.20

Se observa una tasa de acierto homogenea entre los datos de prueba y entrenamiento.

Modelo logístico

Para este modelo se encontraron las variables apropiadas: target_time, pub_rec, loan_amnt, int_rate, open_acc, home_ownership, emp_length.

var_=[
    'target_time', 
      'pub_rec', 
      'loan_amnt', 
      'int_rate', 
       'open_acc', 
       'home_ownership:NONE', 'home_ownership:OTHER', 'home_ownership:OWN',
       'home_ownership:RENT', 
      'emp_length:10+ years', 'emp_length:2 years',
       'emp_length:3 years', 'emp_length:4 years', 'emp_length:5 years',
       'emp_length:6 years', 'emp_length:7 years', 'emp_length:8 years',
       'emp_length:9 years', 'emp_length:< 1 year'
     ]
X_train_pca=X_train[var_]
# X_train_pca=PCA_5.transform(X_train)
X_test_pca=X_test[var_]
# X_test_pca=PCA_5.transform(X_test)
Modelo = LogisticRegression()#C=1e-09,class_weight="balanced",solver="sag")
Modelo.fit(X_train_pca, y_train)
## LogisticRegression()
prop_=0.8
predict_train_log= Modelo.predict_proba(X_train_pca)[:,1]
predict_test_log= Modelo.predict_proba(X_test_pca)[:,1]
predict_train_log=np.where(predict_train_log>=prop_,1,0  )
predict_test_log=np.where(predict_test_log>=prop_,1,0  )
tasa_acierto_1=accuracy_score(y_train, predict_train_log)*100
tasa_acierto_2=accuracy_score(y_test,predict_test_log )*100
result=pd.DataFrame({"Población":["Train", "Test"], "Aciertos": [tasa_acierto_1,tasa_acierto_2 ] })
errores_logistico=pd.concat([pd.crosstab(y_train,predict_train_log ,margins=False, normalize=True, colnames=["predicción"],  rownames=['Real']),
           pd.crosstab(y_test,predict_test_log , margins=False, normalize=True, colnames=["predicción"], rownames=['Real'])
    ],axis=1,keys=["Train", "Test"])
Validación modelo logístico
Población Aciertos
Train 82.23
Test 82.45

Esto es menor al modelo de bayes, luego se deben analizar los tipos de errores en cada modelo.

Tabla comparativa modelo logístico
Train 0 1 Test 0 1
0 4.34 5.71 4.36 5.48
1 12.06 77.90 12.07 78.09
Tabla comparativa modelo Naive Bayes
Train 0 1 Test 0 1
0 4.26 5.78 4.18 5.66
1 9.11 80.85 9.14 81.02